﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Net.Sockets;
using System.Net;
namespace serwer {
    /// <summary>
    /// Obsługuje podłączonego klienta odbierając wiadomości i odpowiadając na nie.
    /// </summary>
    class Client {
        /// <summary>
        /// Identyfikator, który będzie nadany kolejnemu podłączonemu klientowi.
        /// </summary>
        private static int idCounter = 1;

        /// <summary>
        /// Adres IP klienta.
        /// </summary>
        public string IpAddress {
            get {
                return ((IPEndPoint)clientSocket.RemoteEndPoint).Address.ToString();
            }
        }
        /// <summary>
        /// Identyfikator klienta.
        /// </summary>
        public int ClientId {
            get {
                return clientId;
            }
        }
        /// <summary>
        /// Czas trwania połączenia.
        /// </summary>
        public string ConnectionTime {
            get {
                TimeSpan timeSpan = DateTime.Now - startTime;
                int hours = (int)timeSpan.TotalHours;
                string timeString = hours.ToString() + ":" + timeSpan.ToString("mm\\:ss");
                return timeString;
            }
        }
        /// <summary>
        /// True gdy klient jest połączony, false w przeciwnym wypadku.
        /// </summary>
        public bool IsConnected {
            get {
                if (clientThread.ThreadState == ThreadState.Stopped) return false;
                else return true;
            }
        }

        /// <summary>
        /// Ustawiane na true, gdy serwer zakończył połączenie
        /// </summary>
        private bool serverSideDisconnect;
        /// <summary>
        /// Socket, przez który komunikuje się klient.
        /// </summary>
        private Socket clientSocket;
        /// <summary>
        /// Wątek odbierający, przetwarzający i wysyłający odpowiedzi na wiadomości.
        /// </summary>
        private Thread clientThread;
        /// <summary>
        /// Czas rozpoczęcia połączenia.
        /// </summary>
        private readonly DateTime startTime;
        /// <summary>
        /// Identyfikator klienta.
        /// </summary>
        private readonly int clientId;
        /// <summary>
        /// Generator liczb losowych, używany do symulacji obliczeń.
        /// </summary>
        private System.Random rand;
        /// <summary>
        /// Stoper służący do pomiaru czasu trwania obliczeń.
        /// </summary>
        System.Diagnostics.Stopwatch stopwatch;
        /// <summary>
        /// Lista klientów, której elementem jest bieżący klient.
        /// </summary>
        private readonly List<Client> clientList;
        /// <summary>
        /// Event ustawiany, gdy na liście klientów zwolni się miejsce.
        /// </summary>
        ManualResetEventSlim freeSpaceInQueueEvent;

        /// <summary>
        /// Tworzy instancję obiektu odbierającego komunikaty od klienta, przetwarzającego je oraz wysyłającego odpowiedzi.
        /// </summary>
        /// <param name="socket">Socket używany do komunikacji z klientem.</param>
        /// <param name="clientList">Lista klientów, do której dodany został bieżący klient.</param>
        /// <param name="freeSpaceInQueueEvent">Event ustawiany, gdy na liście klientów zwolni się miejsce.</param>
        public Client(Socket socket, List<Client> clientList, ManualResetEventSlim freeSpaceInQueueEvent) {
            serverSideDisconnect = false;
            this.freeSpaceInQueueEvent = freeSpaceInQueueEvent;
            this.clientList = clientList;
            clientSocket = socket;
            clientId = idCounter;
            idCounter++;
            startTime = DateTime.Now;
            rand = new Random();
            stopwatch = new System.Diagnostics.Stopwatch();
            clientThread = new Thread(new ThreadStart(ClientThreadLoop));
            clientThread.Start();
        }
        /// <summary>
        /// Kończy połączenie z klientem.
        /// </summary>
        public void CloseConnection() {
            clientSocket.Close(100);
            clientThread.Join();
        }
        /// <summary>
        /// Symuluje obliczenia wykonywane na otrzymanych danych.
        /// </summary>
        /// <param name="data">Dane do przetworzenia.</param>
        /// <returns>Przetworzone dane.</returns>
        private Message ProcessData(Message data) {
            //symulowane obliczenia
            Thread.Sleep(rand.Next(1000, 10000));
            return data;
        }
        /// <summary>
        /// Wysyła dane do klienta.
        /// </summary>
        /// <param name="message">Dane do wysłania.</param>
        private void Send(object message) {
            if(message == null) throw new ArgumentException("Nie można wysłać NULLa.");
            System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter 
                = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            System.IO.MemoryStream stream = new System.IO.MemoryStream();
            formatter.Serialize(stream, message);
            byte[] serializedMessage = stream.ToArray();
            try {
                clientSocket.Send(serializedMessage);
            }
            catch (Exception ex) {
                throw ex;
            }
        }
        /// <summary>
        /// Odbiera dane od klienta, oczekując w razie potrzeby na pojawienie się ich w buforze.
        /// </summary>
        /// <returns>Odebrane dane.</returns>
        private Message Receive() {
            const int bufferSize = 4096;
            const int waitTimeout = 200;
            int totalByteCount = 0;
            List<byte[]> bufferList = new List<byte[]>();
            try {
                do {
                    byte[] buffer = new byte[bufferSize];
                    int byteCount = clientSocket.Receive(buffer);
                    totalByteCount += byteCount;
                    byte[] receivedBytes = new byte[byteCount];
                    Buffer.BlockCopy(buffer, 0, receivedBytes, 0, byteCount);
                    
                    bufferList.Add(receivedBytes);
                    if (clientSocket.Available == 0)
                        Thread.Sleep(waitTimeout);
                } while (clientSocket.Available != 0);
                byte[] message = new byte[totalByteCount];
                int offset = 0;
                foreach (byte[] b in bufferList) {
                    Buffer.BlockCopy(b, 0, message, offset, b.Length);
                    offset += b.Length;
                }
                System.IO.MemoryStream messageMemoryStream = new System.IO.MemoryStream(message);
                System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter
                        = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
                messageMemoryStream.Position = 0;
                return new Message(totalByteCount, formatter.Deserialize(messageMemoryStream));
            }
            catch (Exception ex) {
                throw ex;
            }
        }
        /// <summary>
        /// Pętla wykonywana przez wątek obsługujący klienta.
        /// </summary>
        private void ClientThreadLoop() {
            while (true) {
                try {
                    Message message = Receive();
                    stopwatch.Restart();
                    message = ProcessData(message);
                    stopwatch.Stop();
                    string replyMessage = "Odebrano " + message.Length + " bajtów.\nCzas przetwarzania: " + stopwatch.ElapsedMilliseconds + "ms.";
                    Send(replyMessage);
                }
                catch (Exception) {
                    break;
                }
            }
            lock (clientList) {
                if(!serverSideDisconnect) clientSocket.Close();
                clientList.Remove(this);
                freeSpaceInQueueEvent.Set();
            }
        }
    }
}
